객체지향 디자인의 목표는 수정 비용을 줄이는 것이다. 애플리케이션 디자인의 핵심은 메시지라는 사실과 엄격하게 정의된 퍼블릭 인터페이스가 얼마나 중요한지를 알고 있다.
오리타입은 특정 클래스에 종속되지 않는 퍼블릭 인터페이스이다. 애플리케이션을 값비싼 의존으로부터 유연하게 만들어준다. 어떤 객체가 하나의 인터페이스에만 반응할 수 있다고 생각할 필요가 없다. 서로 다른 여러 개의 인터페이스를 구현할 수 있다. 진짜 중요한 것은 객체가 무엇인가가 아니라 어떻게 행동하는가이다. 모든 상황에서 모든 객체가 예상한 바대로 움직인다고 믿을 수 있다면, 어떤 타입이든 될 수 있다고 믿을 수 있다면 디자인의 무한한 가능성이 보일 것이다. 하지만 반대로 해독할 수 없는 혼란하고 끔찍한 디자인을 만들어 낼 수도 있다.
class Trip: def __init__(self, bicycles, customers, vehicle): self.bicycles = bicycles self.customers = customers self.vehicle = vehicle def prepare(mechanic): mechanic.prepare_bicycles(bicycles) class Mechanic: def prepare_bicycle(bicycles): for bicycle in bicycles: prepare_bicycle(bicycle) def prepare_bicycle(bicycle): ...
prepare 메서드 자체는 Mechanic 클래스에 의존하고 있지 않다. 하지만 prepare_bicycles 메서드에 반응할 수 있는 개체를 수신해야 한다는 사실에 의존하고 있다. Trip의 prepare 메서드는 여행준비를 담당하는 객체(여기선 mechanic)를 인자로 받았다고 확신하고 있다.
여행 준비가 더 복잡해진다면
class Trip: def __init__(self, bicycles, customers, vehicle): self.bicycles = bicycles self.customers = customers self.vehicle = vehicle def prepare(self, preparers): for preparer in preparers: if isinstance(preparer, Mechanic): preparer.prepare_bicycles(self.bicycles) elif isinstance(preparer, TripCoordinator): preparer.buy_food(self.customers) elif isinstance(preparer, Driver): preparer.gas_up(self.vehicle) preparer.fill_water_tank(self.vehicle) class Mechanic: def prepare_bicycles(self, bicycles): for bicycle in bicycles: self.prepare_bicycle(bicycle) def prepare_bicycle(self, bicycle): # ... pass class TripCoordinator: def buy_food(self, customers): # ... pass class Driver: def gas_up(self, vehicle): # ... pass def fill_water_tank(self, vehicle): # ... pass
요구 사항이 변경되어, 여행 준비에 정비공 뿐아니라 여행 보조인과 운전수도 필요해졌다. 그리고 각각에 어울리는 책임도 부여했다. 새로 만든 클래스의 책임은 간단하고 괜찮아 보인다. 하지만 Trip 클래스의 prepare 메서드는 문제가 있다. 세개의 서로 다른 클래스를 참조하고 있고, 각 클래스가 구현하고 있는 메서드의 이름을 정확히 알고 있다.
오리타입 찾기
이런 의존성을 제거하기 위해서는 의도를 파악해야 한다. prepare 메서드는 하나의 의도를 가지고 있다. 내부의 모든 인자는 같은 사명을 띄고 모였다. 우리는 ‘인자의 클래스가 무엇을 할 줄 아는지’ 알고 있다. ‘prepare가 무엇을 원하는지‘에 집중하자. prepare 메서드는 여행을 준비하고 싶어한다.
class Trip: def __init__(self, bicycles, customers, vehicle): self.bicycles = bicycles self.customers = customers self.vehicle = vehicle def prepare(self, preparers): for preparer in preparers: preparer.prepare_trip(self) class Mechanic: def prepare_trip(self, trip): for bicycle in trip.bicycles: self.prepare_bicycle(bicycle) def prepare_bicycle(self, bicycle): print(f"Preparing bicycle {bicycle}") class TripCoordinator: def prepare_trip(self, trip): self.buy_food(trip.customers) def buy_food(self, customers): print(f"Buying food for {len(customers)} customers") class Driver: def prepare_trip(self, trip): vehicle = trip.vehicle self.gas_up(vehicle) self.fill_water_tank(vehicle) def gas_up(self, vehicle): print(f"Gassing up the vehicle {vehicle}") def fill_water_tank(self, vehicle): print(f"Filling the water tank of the vehicle {vehicle}") bicycles = ['Bicycle1', 'Bicycle2'] customers = ['Customer1', 'Customer2'] vehicle = 'Vehicle1' trip = Trip(bicycles, customers, vehicle) mechanic = Mechanic() trip_coordinator = TripCoordinator() driver = Driver() preparers = [mechanic, trip_coordinator, driver] trip.prepare(preparers)
이제 prepare 메서드를 구정하지 않고도 새로운 Preparer를 추가할 수 있다. 여행 준비가 복잡해져도 손쉽게 새로운 Preparer를 추가할 수 있다.
오리 타입을 사용해서 얻는 이점
최초 구체 클래스는 구체적이기 때문에 이해가 쉽지만 확장에는 불리하다. 이를 오리 타입에 의존하게 하면서 확장과 수정에 용이하도록 했다. 객체를 클래스에 정의된 것이 아니라 행동을 통해 정의된 것으로 이해하기 시작할 때 우리는 표현력있고 유연한 디자이너가 될 수 있다.
숨겨진 오리타입 찾아내기
클래스에 따라 변경되는 조건문
class Trip: def __init__(self, bicycles, customers, vehicle): self.bicycles = bicycles self.customers = customers self.vehicle = vehicle def prepare(self, preparers): for preparer in preparers: if isinstance(preparer, Mechanic): preparer.prepare_bicycles(self.bicycles) elif isinstance(preparer, TripCoordinator): preparer.buy_food(self.customers) elif isinstance(preparer, Driver): preparer.gas_up(self.vehicle) preparer.fill_water_tank(self.vehicle) class Mechanic: def prepare_bicycles(self, bicycles): pass class TripCoordinator: def buy_food(self, customers): pass class Driver: def gas_up(self, vehicle): pass def fill_water_tank(self, vehicle): pass
isinstance()
if isinstance(preparer, Mechanic): preparer.prepare_bicycles(bicycles) elif isinstance(preparer, TripCoordinator): preparer.buy_food(customers) elif isinstance(preparer, Driver): preparer.gas_up(vehicle) preparer.fill_water_tank(vehicle)
hasattr()
if hasattr(preparer, 'prepare_bicycles'): preparer.prepare_bicycles(bicycles) elif hasattr(preparer, 'buy_food'): preparer.buy_food(customers) elif hasattr(preparer, 'gas_up'): preparer.gas_up(vehicle) preparer.fill_water_tank(vehicle)
댓글